/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 *
 * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
 *
 * Contributor(s): 
 *  Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
 *  
 * If you didn't download this code from the following link, you should check
 * if you aren't using an obsolete version: http://www.isqlviewer.com
 */
package org.isqlviewer.history;

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.io.Serializable;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Vector;

import org.isqlviewer.FieldNotModifiableException;
import org.isqlviewer.sql.embedded.EmbeddedDatabase;
import org.isqlviewer.util.Assertions;
import org.isqlviewer.util.BasicUtilities;

/**
 * Generic command object for logging command history within iSQL-Viewer.
 * <p>
 * 
 * @author Mark A. Kobold &lt;mkobold at isqlviewer dot com&gt;
 * @version 1.0
 */
public class HistoricalCommand implements Serializable, Transferable {

    /**
     * Standard data-transfer flavor for DnD operations.
     */
    public static final DataFlavor FLAVOR = new DataFlavor(HistoricalCommand.class, "iSQL-Viewer Historical Command");
    private static final long serialVersionUID = -4661533009593760894L;

    private String commandText = null;
    private String service = null;
    private long id = -1;
    private long transactionId = -1;
    private CommandType type = CommandType.NOT_DEFINED;
    private Date queryTime = null;
    private Vector<HistoricalCommand> subCommands = new Vector<HistoricalCommand>();

    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {

        if (flavor != null) {
            EmbeddedDatabase database = EmbeddedDatabase.getSharedInstance();
            HistoricalCommand command = null;
            try {
                if (hasChildCommands()) {
                    command = database.getHistoricalCommand(this);
                } else {
                    command = database.getHistoricalCommand(getId());
                }
            } catch (SQLException sourceError) {
                IOException wrappedError = new IOException(sourceError.getMessage());
                BasicUtilities.wrapThrowable(sourceError, wrappedError);
                throw wrappedError;
            }
            StringBuilder userCommand = new StringBuilder();
            if (hasChildCommands()) {
                for (HistoricalCommand child : command.subCommands) {
                    userCommand.append(child.getCommandText());
                    userCommand.append(";\n");
                }
                command.setCommandText(userCommand.toString());
            }
            if (HistoricalCommand.FLAVOR.equals(flavor)) {
                return command;
            } else if (flavor.isFlavorTextType()) {
                return command.getCommandText();
            }
        }
        throw new UnsupportedFlavorException(flavor);
    }

    public DataFlavor[] getTransferDataFlavors() {

        return new DataFlavor[]{FLAVOR, HistoricalCommand.FLAVOR, DataFlavor.stringFlavor};
    }

    public boolean isDataFlavorSupported(DataFlavor flavor) {

        if (flavor != null) {
            if (FLAVOR.equals(flavor) || HistoricalCommand.FLAVOR.equals(flavor) || flavor.isFlavorTextType()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {

        return (int) (id ^ (id >>> 32));
    }

    @Override
    public boolean equals(Object other) {

        if (other instanceof HistoricalCommand) {
            HistoricalCommand hr = (HistoricalCommand) other;
            return hr.id == id;
        }
        return false;
    }

    @Override
    public String toString() {

        Object[] arguments = new Object[3];
        arguments[0] = Long.toString(id);
        arguments[1] = Long.toString(transactionId);
        arguments[2] = type.name();
        return MessageFormat.format("HistoricalCommand[id=''{0}'', txnId=''{1}'', type=''{2}'']", arguments);
    }

    /**
     * Gets the execution time at which this event occured.
     * <p>
     * 
     * @return timestamp at which this event happened as Java Date; can be null if not previously set.
     */
    public Date getQueryTime() {

        return queryTime == null ? null : (Date) queryTime.clone();
    }

    /**
     * Sets the time this command was executed at in millis.
     * <p>
     * 
     * @param queryTime as a Java date object.
     * @throws NullPointerException if the given queryTime value is null.
     */
    public void setQueryTime(Date queryTime) {

        Assertions.assertNotNull("history-query-time", queryTime);
        this.queryTime = (Date) queryTime.clone();
    }

    /**
     * Gets the unique identifier for this history reference.
     * <p>
     * Simply number that unique identifies this service reference within the iSQL-Viewer application. If the Id is -1
     * or less than 0, then that indicates that this particular history reference instance is not registered within the
     * application.
     * <p>
     * This value has no real value other than uniquely identifying this historical event.
     * 
     * @return the id of this historical command; can be negative if not previously set.
     */
    public long getId() {

        return id;
    }

    /**
     * Sets the service reference identifier to the specified value.
     * <p>
     * 
     * @param id the id to set to uniquely identify this service.
     * @throws IllegalArgumentException if the given id is not a positive number.
     * @throws FieldNotModifiableException if the id for this instance has already been set.
     */
    public void setId(long id) {

        Assertions.assertPositive("history-reference-id", id);
        if (this.id == -1) {
            this.id = id;
        } else {
            throw new FieldNotModifiableException("history-reference-id");
        }
    }

    /**
     * Gets the type of historical event this instance represents.
     * <p>
     * 
     * @return the type of command this instance is.
     */
    public CommandType getType() {

        return type;
    }

    /**
     * Sets the type of command this event represents.
     * <p>
     * 
     * @param type the event type for this instance.
     * @throws NullPointerException if the given type is null.
     * @throws FieldNotModifiableException if the type has been previously set.
     */
    public void setType(CommandType type) {

        Assertions.assertNotNull("history-reference-type", type);
        if (this.type == CommandType.NOT_DEFINED) {
            this.type = type;
        } else {
            throw new FieldNotModifiableException("history-reference-type");
        }
    }

    /**
     * Gets the transaction id for this event.
     * <p>
     * If multiple events occur within the same transaction they will all be associated with the same transaction id.
     * This id has no real value in its solitary form.
     * 
     * @return the transactionId for this event; can be negative if not set or is not applicable to this event.
     */
    public long getTransactionId() {

        return transactionId;
    }

    /**
     * Sets the transactional id for this event .
     * <p>
     * 
     * @param transactionId to set for this event.
     * @throws IllegalArgumentException if the given transaction id is negative; with the exception -1;
     */
    public void setTransactionId(long transactionId) {

        if (transactionId != -1) {
            Assertions.assertPositive("history-reference-transaction-id", transactionId);
        }
        this.transactionId = transactionId;
    }

    /**
     * Gets the commandText of this command that was executed.
     * <p>
     * 
     * @return commandText of this command; can be null if unavailable.
     */
    public String getCommandText() {

        return commandText;
    }

    /**
     * Sets the raw command text used for this command.
     * <p>
     * This is typically either the raw text of the command like the SQL statement executed. This field may not be
     * available for all types of commands where the command cannot be effectively reproduced. Such as when a
     * transaction was started by setting the auto-commit state to false on the connection.
     * 
     * @param commandText of the command being executed; can be null if no text command was executed.
     */
    public void setCommandText(String content) {

        this.commandText = content;
    }

    /**
     * Sets the name of the iSQL-Viewer service this historical command is associated with.
     * <p>
     * 
     * @param service name to set for this event.
     * @throws NullPointerException if the given service is null.
     */
    public void setService(String service) {

        Assertions.assertNotNull("historical-command-service", service);
        this.service = service;
    }

    /**
     * Sets the name of the iSQL-Viewer service this historical command is associated with.
     * <p>
     * 
     * @return the service that this event was executed against; can be null if not previously set.
     */
    public String getService() {

        return service;
    }

    /**
     * Add sub-command to this command context.
     * <p>
     * Adds a child command that effectively puts it as a child process of sorts. Not all command types can have
     * subCommands however, and command types may only allow specific child types of commands.
     * 
     * @param subcommand to add to this event.
     * @return <tt>true</tt> if the subcommand was successfully added to this event.
     * @throws NullPointerException if the subcommand is null.
     * @throws IllegalArgumentException if the subcommand transaction is not equal to this instances' transaction id.
     */
    public boolean addSubcommand(HistoricalCommand subcommand) {

        long txnID = getTransactionId();
        Assertions.assertNotNull("historical-command-subcommand", subcommand);
        Assertions.assertEqual("history-subcommand-transaction-id", txnID, subcommand.getTransactionId());
        return subCommands.add(subcommand);
    }

    /**
     * Acquires enumeration of child process within the command context.
     * <p>
     * 
     * @return enumerated child commands that were executed as a part of this instance.
     */
    public Enumeration<HistoricalCommand> elements() {

        return subCommands.elements();
    }

    /**
     * Gets child command by index.
     * <p>
     * 
     * @param index of the desired child command.
     * @return the command at the given index.
     * @throws ArrayIndexOutOfBoundsException if index is invalid.
     */
    public HistoricalCommand getChildCommand(int index) {

        return subCommands.get(index);
    }

    /**
     * Get the index of the child command in this command context.
     * <p>
     * 
     * @param childCommand to seek the index of within this instance.
     * @return index of the command in this instance; -1 if not found.
     */
    public int indexOf(HistoricalCommand childCommand) {

        return subCommands.indexOf(childCommand);
    }

    /**
     * Gets the number of child commands that were part of this instance.
     * <p>
     * 
     * @return number of child processes that were executed in this command context.
     */
    public int size() {

        return subCommands.size();
    }

    /**
     * Determines if this historical command has child commands.
     * <p>
     * 
     * @return <tt>true</tt> if this command instance contains child processes.
     */
    public boolean hasChildCommands() {

        return !subCommands.isEmpty();
    }

    public int getChildCount() {

        return subCommands.size();
    }
}